package org.hackystat.sensor.ant.findbugs;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.hackystat.sensor.ant.findbugs.resource.jaxb.BugCollection;
import org.hackystat.sensor.ant.findbugs.resource.jaxb.BugInstance;
import org.hackystat.sensor.ant.util.LongTimeConverter;
import org.hackystat.sensorshell.SensorProperties;
import org.hackystat.sensorshell.SensorPropertiesException;
import org.hackystat.sensorshell.SensorShell;
import org.hackystat.sensorshell.usermap.SensorShellMap;
import org.hackystat.sensorshell.usermap.SensorShellMapException;
import org.hackystat.utilities.tstamp.TstampSet;

/**
 * Implements an Ant task that parses the XML files generated by FindBugs and sends the test case
 * results to the Hackystat server.
 * 
 * @author Philip Johnson, Hongbing Kou, Joy Agustin, Julie Ann Sakuda, Aaron A. Kagawa
 * @version $Id: FindBugsSensor.java,v 1.1.1.1 2005/10/20 23:56:58 johnson Exp $
 */
public class FindBugsSensor extends Task {

  /** The list of all XML filesets generated by the FindBugs task. */
  private ArrayList<FileSet> filesets;
  /** Whether or not to print out messages during FindBugs send. */
  private boolean verbose = false;
  /** Tool in UserMap to use. */
  private String tool;
  /** Tool account in the UserMap to use. */
  private String toolAccount;

  /** The sensor shell instance used by this sensor. */
  private SensorShell sensorShell;
  /** Sensor properties to be used with the sensor. */
  private SensorProperties sensorProps;
  /** Timestamp set that will guarantee uniqueness of FindBugs timestamps. */
  private TstampSet tstampSet;

  /** Initialize a new instance of a FindBugsSensor. */
  public FindBugsSensor() {
    this.filesets = new ArrayList<FileSet>();
    this.tstampSet = new TstampSet();
  }
  
  /**
   * Initialize a new instance of a FindBugsSensor, passing the host email, and password 
   * directly. This supports testing. Note that when this constructor is called, offline 
   * data recovery by the sensor is disabled.
   * 
   * @param host The hackystat host URL.
   * @param email The Hackystat email to use.
   * @param password The Hackystat password to use.
   */
  public FindBugsSensor(String host, String email, String password) {
    this.filesets = new ArrayList<FileSet>();
    this.sensorProps = new SensorProperties(host, email, password);
    this.sensorShell = new SensorShell(this.sensorProps, false, "test", false);
    this.tstampSet = new TstampSet();
  }

  /**
   * Set the verbose attribute to "on", "true", or "yes" to enable trace messages while the FindBugs
   * sensor is running.
   * 
   * @param mode The new verbose value: should be "on", "true", or "yes" to enable.
   */
  public void setVerbose(String mode) {
    this.verbose = Project.toBoolean(mode);
  }

  /**
   * Allows the user to specify the tool in the UserMap that should be used when sending data. Note
   * that setting the tool will only have an effect if the tool account is also specified. Otherwise
   * it will be ignored and the values in v8.sensor.properties will be used.
   * 
   * @param tool The tool containing the tool account to be used when sending data.
   */
  public void setUserMapTool(String tool) {
    this.tool = tool;
  }

  /**
   * Allows the user to specify the tool account in the UserMap under the given tool to use when
   * sending data. Note that setting the tool account will only have an effect if the tool is also
   * specified. Otherwise the tool account will be ignored and v8.sensor.properties file values will
   * be used.
   * 
   * @param toolAccount The tool account in the UserMap to use when sending data.
   */
  public void setUserMapToolAccount(String toolAccount) {
    this.toolAccount = toolAccount;
  }

  /**
   * Parses the FindBugs XML files and sends the resulting FindBugs test case results to 
   * the hackystat server. This method is invoked automatically by Ant.
   * 
   * @throws BuildException If there is an error.
   */
  @Override
  public void execute() throws BuildException {
    setupSensorShell();

    int numberOfTests = 0;

    Date startTime = new Date();
    try {
      // Get the file names from the FileSet directives.
      ArrayList<File> files = getFiles();

      // Iterate though each file, extract the FindBugs data, send to sensorshell.
      for (Iterator<File> i = files.iterator(); i.hasNext();) {
        // get full path of next file to process
        String findBugsXmlFile = i.next().getPath();
        if (this.verbose) {
          System.out.println("Processing file: " + findBugsXmlFile);
        }
        numberOfTests += processFindBugsXmlFile(findBugsXmlFile);
      }

      if (send() > 0) {
        Date endTime = new Date();
        long elapsedTime = (endTime.getTime() - startTime.getTime()) / 1000;
        if (isUsingUserMap()) {
          // no sensorProps exists because we used the sensorshell map
          System.out.println("Hackystat data on " + numberOfTests + " FindBugs code issues sent to "
              + "host stored in UserMap with tool '" + this.tool + "' and tool account '"
              + this.toolAccount + "' (" + elapsedTime + " secs.)");
        }
        else {
          System.out.println("Hackystat data on " + numberOfTests + " FindBugs tests sent to "
              + this.sensorProps.getHackystatHost() + " (" + elapsedTime + " secs.)");
        }
      }
      else if (numberOfTests == 0) {
        System.out.println("No data to send.");
      }
      else {
        System.out.println("Failed to send Hackystat FindBugs test data.");
      }
    }
    catch (Exception e) {
      throw new BuildException("Errors occurred while processing the FindBugs report file " + e);
    }
    finally { // After send-out, close the sensor shell.
      this.sensorShell.quit();
    }
  }

  /**
   * Sets up the sensorshell instance to use either based on the given tool & tool account or from
   * the sensor.properties file. DO NOT call this method in the constructor. The optional properties
   * tool and tool account do not get set until after the constructor is done.
   */
  private void setupSensorShell() {
    if (isUsingUserMap()) {
      try {
        SensorShellMap map = new SensorShellMap(this.tool);
        this.sensorShell = map.getUserShell(this.toolAccount);
      }
      catch (SensorShellMapException e) {
        throw new BuildException(e.getMessage(), e);
      }
    }
    // sanity check to make sure the prop and shell haven't already been set by the
    // constructor that takes in the email, password, and host
    else if (this.sensorProps == null && this.sensorShell == null) {
      // use the sensor.properties file
      try {
        this.sensorProps = new SensorProperties();
        this.sensorShell = new SensorShell(this.sensorProps, false, "FindBugs");
      }
      catch (SensorPropertiesException e) {
        System.out.println(e.getMessage());
        System.out.println("Exiting...");
        throw new BuildException(e.getMessage(), e);
      }

      if (!this.sensorProps.isFileAvailable()) {
        System.out.println("Could not find sensor.properties file. ");
        System.out.println("Expected in: " + this.sensorProps.getAbsolutePath());
      }
    }
  }

  /**
   * Gets whether or not this sensor instance is using a mapping in the UserMap.
   * @return Returns true of the tool and tool account are set, otherwise false.
   */
  private boolean isUsingUserMap() {
    return (this.tool != null && this.toolAccount != null);
  }

  /**
   * Sends any accumulated data in the SensorShell to the server.
   * @return Returns the number of SensorData instances sent to the server.
   */
  public int send() {
    return this.sensorShell.send();
  }

  /**
   * Parses a FindBugs XML file and sends the FindBugsEntry instances to the shell.
   * 
   * @param fileNameString The XML file name to be processed.
   * @exception BuildException if any error.
   * @return The number of test cases in this XML file.
   */
  public int processFindBugsXmlFile(String fileNameString) throws BuildException {
    XMLGregorianCalendar runtimeGregorian = 
      LongTimeConverter.convertLongToGregorian(new Date().getTime());
    File xmlFile = new File(fileNameString);
    // The start time for all entries will be approximated by the XML file's last mod time.
    // The shell will ensure that it's unique by tweaking the millisecond field.
    long startTime = xmlFile.lastModified();

    try {
      JAXBContext context = JAXBContext
          .newInstance(org.hackystat.sensor.ant.findbugs.resource.jaxb.ObjectFactory.class);
      Unmarshaller unmarshaller = context.createUnmarshaller();

      BugCollection bugCollection = (BugCollection) unmarshaller.unmarshal(xmlFile);
      Set<String> allSrcFiles = new HashSet<String>(bugCollection.getProject().getSrcDir());
      
      Set<String> srcFilesWithBugInstances = new HashSet<String>();
      int codeIssueCount = 0;
      List<BugInstance> bugInstanceCollection = bugCollection.getBugInstance();
      for (BugInstance bugInstance : bugInstanceCollection) {
        String type = bugInstance.getType();
        int priority = bugInstance.getPriority();
        String category = bugInstance.getCategory();
        String shortMessage = bugInstance.getShortMessage();
        
        String className = bugInstance.getClazz().getClassname();
        int sourceLine = bugInstance.getSourceLine().getStart();
        String sourcePath = bugInstance.getSourceLine().getSourcepath();
        String fullSourcePath = this.findSrcFile(allSrcFiles, sourcePath);
        srcFilesWithBugInstances.add(fullSourcePath);
        
        // Alter startTime to guarantee uniqueness.
        long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);

        // Get altered start time as XMLGregorianCalendar
        XMLGregorianCalendar timestamp = 
          LongTimeConverter.convertLongToGregorian(uniqueTstamp);

        Map<String, String> keyValMap = new HashMap<String, String>();
        keyValMap.put("Tool", "FindBugs");
        keyValMap.put("SensorDataType", "CodeIssue");

        // Required
        keyValMap.put("Runtime", runtimeGregorian.toString());
        keyValMap.put("Timestamp", timestamp.toString());
        keyValMap.put("Resource", fullSourcePath);
        
        // Optional
        keyValMap.put("Type", type);
        keyValMap.put("Category", category);
        keyValMap.put("Severity", String.valueOf(priority));
        keyValMap.put("Message", shortMessage);
        keyValMap.put("Line", String.valueOf(sourceLine));
        keyValMap.put("ClassName", className);
        
        this.sensorShell.add(keyValMap); // add data to sensorshell
        codeIssueCount++;
      }
      
      //process the zero issues 
      allSrcFiles.removeAll(srcFilesWithBugInstances);
      for (String srcFile : allSrcFiles) {
        // Alter startTime to guarantee uniqueness.
        long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);

        // Get altered start time as XMLGregorianCalendar
        XMLGregorianCalendar timestamp = 
          LongTimeConverter.convertLongToGregorian(uniqueTstamp);

        Map<String, String> keyValMap = new HashMap<String, String>();
        keyValMap.put("Tool", "FindBugs");
        keyValMap.put("SensorDataType", "CodeIssue");

        // Required
        keyValMap.put("Runtime", runtimeGregorian.toString());
        keyValMap.put("Timestamp", timestamp.toString());
        keyValMap.put("Resource", srcFile);
        this.sensorShell.add(keyValMap); // add data to sensorshell
        codeIssueCount++;
      }
      
      return codeIssueCount;
    }
    catch (Exception e) {
      e.printStackTrace();
      throw new BuildException("Failed to process " + fileNameString + "   " + e);
    }
  }
  
  /**
   * Finds the full file path of the source path within the src files collection. For example, 
   *   srcFiles could contain: [c:\foo\src\org\Foo.java, c:\foo\src\org\Bar.java] and the 
   *   sourcePath could be org\Foo.java.  This method will find and return the full path of the 
   *   Foo.java file. 
   * @param srcFiles Contains the full path to the files. 
   * @param sourcePath Contains a trimmed version of a file path. 
   * @return The full file path, or null if the path is not found. 
   */
  private String findSrcFile(Set<String> srcFiles, String sourcePath) {
    for (String srcFile : srcFiles) {
      if (srcFile == null) {
        continue;
      }
      String alteredSourcePath = sourcePath;
      if (srcFile.contains("\\")) {
        alteredSourcePath = sourcePath.replace('/', '\\');
      }
      if (srcFile != null && srcFile.contains(alteredSourcePath)) {
        return srcFile; 
      }
    }
    return null;
  }
  

  /**
   * Add a fileset which contains the FindBugs report xml file to be processed.  
   *   Invoked automatically by Ant.
   * @param fs The new fileset of xml results.
   */
  public void addFileSet(FileSet fs) {
    this.filesets.add(fs);
  }

  /**
   * Returns all of the files in the fileset.
   * @return All files in the fileset.
   */
  private ArrayList<File> getFiles() {
    ArrayList<File> fileList = new ArrayList<File>();
    final int size = this.filesets.size();
    for (int i = 0; i < size; i++) {
      FileSet fs = this.filesets.get(i);
      DirectoryScanner ds = fs.getDirectoryScanner(getProject());
      ds.scan();
      String[] f = ds.getIncludedFiles();

      for (int j = 0; j < f.length; j++) {
        String pathname = f[j];
        File file = new File(ds.getBasedir(), pathname);
        file = getProject().resolveFile(file.getPath());
        fileList.add(file);
      }
    }
    return fileList;
  }
}